/**********************************************************************

  Audacity: A Digital Audio Editor

  LabelTrack.cpp

  Dominic Mazzoni
  James Crook
  Jun Wan

*******************************************************************//**

\class LabelTrack
\brief A LabelTrack is a Track that holds labels (LabelStruct).

These are used to annotate a waveform.
Each label has a start time and an end time.
The text of the labels is editable and the
positions of the end points are draggable.

*//****************************************************************//**

\class LabelStruct
\brief A LabelStruct holds information for ONE label in a LabelTrack

LabelStruct also has label specific functions, mostly functions
for drawing different aspects of the label and its text box.

*//*******************************************************************/

#include "LabelTrack.h"

#include <algorithm>
#include <limits.h>
#include <float.h>

#include <wx/log.h>
#include <wx/tokenzr.h>
#include <wx/datetime.h>

#include "Prefs.h"
#include "Project.h"

#include "TimeWarper.h"
#include "BasicUI.h"

const FileNames::FileType LabelTrack::SubripFiles{ XO("SubRip text file"), { wxT("srt") }, true };
const FileNames::FileType LabelTrack::WebVTTFiles{ XO("WebVTT file"), { wxT("vtt") }, true };

EnumSetting<bool> LabelStyleSetting {
    wxT("/FileFormats/LabelStyleChoice"),
    {
        EnumValueSymbol { wxT("Standard"), XXO("S&tandard") },
        EnumValueSymbol { wxT("Extended"),
                          XXO("E&xtended (with frequency ranges)") },
    },
    0, // true

    {
        true,
        false,
    },
};

static int64_t s_lastLabelId = 0;

int64_t LabelStruct::NewID()
{
    return ++s_lastLabelId;
}

int64_t LabelStruct::GetId() const
{
    return mId;
}

void LabelStruct::SetId(int64_t id)
{
    mId = id;
}

LabelTrack::Interval::~Interval() = default;

double LabelTrack::Interval::Start() const
{
    return mpTrack->GetLabel(index)->selectedRegion.t0();
}

double LabelTrack::Interval::End() const
{
    return mpTrack->GetLabel(index)->selectedRegion.t1();
}

size_t LabelTrack::Interval::NChannels() const
{
    return 1;
}

std::shared_ptr<ChannelInterval>
LabelTrack::Interval::DoGetChannel(size_t iChannel)
{
    if (iChannel == 0) {
        return std::make_shared<ChannelInterval>();
    }
    return {};
}

static ProjectFileIORegistry::ObjectReaderEntry readerEntry{
    "labeltrack",
    LabelTrack::New
};

wxString LabelTrack::GetDefaultName()
{
    return _("Labels");
}

LabelTrack* LabelTrack::New(AudacityProject& project)
{
    auto& tracks = TrackList::Get(project);
    auto result = tracks.Add(std::make_shared<LabelTrack>());
    result->AttachedTrackObjects::BuildAll();
    return result;
}

LabelTrack* LabelTrack::Create(TrackList& trackList, const wxString& name)
{
    auto track = std::make_shared<LabelTrack>();
    track->SetName(name);
    trackList.Add(track);
    return track.get();
}

LabelTrack* LabelTrack::Create(TrackList& trackList)
{
    return Create(trackList, trackList.MakeUniqueTrackName(GetDefaultName()));
}

LabelTrack::Holder LabelTrack::CreatePtr(TrackList& trackList)
{
    auto track = std::make_shared<LabelTrack>();
    track->SetName(trackList.MakeUniqueTrackName(GetDefaultName()));
    trackList.Add(track);
    return track;
}

LabelTrack::LabelTrack()
    : UniqueChannelTrack{}
    , mClipLen{0.0}
    , miLastLabel{-1}
{
}

LabelTrack::LabelTrack(const LabelTrack& orig, ProtectedCreationArg&& a)
    : UniqueChannelTrack{orig, std::move(a)}
    , mClipLen{0.0}
{
    for (auto& original: orig.mLabels) {
        LabelStruct l { original.selectedRegion, original.title };
        l.SetId(LabelStruct::NewID());
        mLabels.push_back(l);
    }
}

static const Track::TypeInfo& typeInfo()
{
    static Track::TypeInfo info{
        { "label", "label", XO("Label Track") }, true, &Track::ClassTypeInfo() };
    return info;
}

auto LabelTrack::GetTypeInfo() const -> const TypeInfo&
{
    return typeInfo();
}

auto LabelTrack::ClassTypeInfo() -> const TypeInfo&
{
    return typeInfo();
}

Track::Holder LabelTrack::PasteInto(AudacityProject&, TrackList& list) const
{
    auto pNewTrack = std::make_shared<LabelTrack>();
    pNewTrack->Init(*this);
    pNewTrack->Paste(0.0, *this, false);
    list.Add(pNewTrack);
    return pNewTrack;
}

size_t LabelTrack::NIntervals() const
{
    return mLabels.size();
}

auto LabelTrack::MakeInterval(size_t index) -> std::shared_ptr<Interval>
{
    if (index >= mLabels.size()) {
        return {};
    }
    return std::make_shared<Interval>(*this, index);
}

std::shared_ptr<WideChannelGroupInterval>
LabelTrack::DoGetInterval(size_t iInterval)
{
    return MakeInterval(iInterval);
}

void LabelTrack::SetLabel(size_t iLabel, const LabelStruct& newLabel)
{
    if (iLabel >= mLabels.size()) {
        wxASSERT(false);
        mLabels.resize(iLabel + 1);
    }
    mLabels[ iLabel ] = newLabel;
}

LabelTrack::~LabelTrack()
{
}

void LabelTrack::MoveTo(double origin)
{
    if (!mLabels.empty()) {
        const auto offset = origin - mLabels[0].selectedRegion.t0();
        for (auto& labelStruct: mLabels) {
            labelStruct.selectedRegion.move(offset);
        }
    }
}

void LabelTrack::ShiftBy(double t0, double delta)
{
    if (mLabels.empty()) {
        return;
    }
    for (auto& labelStruct: mLabels) {
        if (labelStruct.selectedRegion.t0() >= t0) {
            labelStruct.selectedRegion.move(delta);
        }
    }
}

void LabelTrack::Clear(double b, double e, bool moveClips)
{
    // May DELETE labels, so use subscripts to iterate
    for (size_t i = 0; i < mLabels.size(); ++i) {
        auto& labelStruct = mLabels[i];
        LabelStruct::TimeRelations relation
            =labelStruct.RegionRelation(b, e, this);
        if (relation == LabelStruct::BEFORE_LABEL) {
            labelStruct.selectedRegion.move(-(e - b));
        } else if (relation == LabelStruct::SURROUNDS_LABEL) {
            DeleteLabel(i);
            --i;
        } else if (relation == LabelStruct::ENDS_IN_LABEL) {
            labelStruct.selectedRegion.setTimes(
                b,
                labelStruct.getT1() - (e - b));
        } else if (relation == LabelStruct::BEGINS_IN_LABEL) {
            labelStruct.selectedRegion.setT1(b);
        } else if (relation == LabelStruct::WITHIN_LABEL) {
            labelStruct.selectedRegion.moveT1(-(e - b));
        }
    }
}

bool LabelTrack::SplitDelete(double b, double e)
{
    // May DELETE labels, so use subscripts to iterate

    std::vector<size_t> labelsToDelete;

    for (size_t i = 0, len = mLabels.size(); i < len; ++i) {
        auto& labelStruct = mLabels[i];
        LabelStruct::TimeRelations relation = labelStruct.RegionRelation(b, e, this);
        if (relation == LabelStruct::SURROUNDS_LABEL) {
            labelsToDelete.push_back(i);
        } else if (relation == LabelStruct::WITHIN_LABEL) {
            labelStruct.selectedRegion.moveT1(-(e - b));
        } else if (relation == LabelStruct::ENDS_IN_LABEL) {
            labelStruct.selectedRegion.setT0(e);
        } else if (relation == LabelStruct::BEGINS_IN_LABEL) {
            labelStruct.selectedRegion.setT1(b);
        }
    }

    for (auto it = labelsToDelete.rbegin(); it != labelsToDelete.rend(); ++it) {
        DeleteLabel(*it);
    }

    return true;
}

void LabelTrack::ShiftLabelsOnInsert(double length, double pt)
{
    for (auto& labelStruct: mLabels) {
        LabelStruct::TimeRelations relation
            =labelStruct.RegionRelation(pt, pt, this);

        if (relation == LabelStruct::BEFORE_LABEL) {
            labelStruct.selectedRegion.move(length);
        } else if (relation == LabelStruct::WITHIN_LABEL) {
            labelStruct.selectedRegion.moveT1(length);
        }
    }
}

void LabelTrack::ChangeLabelsOnReverse(double b, double e)
{
    for (auto& labelStruct: mLabels) {
        if (labelStruct.RegionRelation(b, e, this)
            == LabelStruct::SURROUNDS_LABEL) {
            double aux     = b + (e - labelStruct.getT1());
            labelStruct.selectedRegion.setTimes(
                aux,
                e - (labelStruct.getT0() - b));
        }
    }
    SortLabels();
}

void LabelTrack::ScaleLabels(double b, double e, double change)
{
    for (auto& labelStruct: mLabels) {
        labelStruct.selectedRegion.setTimes(
            AdjustTimeStampOnScale(labelStruct.getT0(), b, e, change),
            AdjustTimeStampOnScale(labelStruct.getT1(), b, e, change));
    }
}

double LabelTrack::AdjustTimeStampOnScale(double t, double b, double e, double change)
{
//t is the time stamp we'll be changing
//b and e are the selection start and end

    if (t < b) {
        return t;
    } else if (t > e) {
        double shift = (e - b) * change - (e - b);
        return t + shift;
    } else {
        double shift = (t - b) * change - (t - b);
        return t + shift;
    }
}

// Move the labels in the track according to the given TimeWarper.
// (If necessary this could be optimised by ignoring labels that occur before a
// specified time, as in most cases they don't need to move.)
void LabelTrack::WarpLabels(const TimeWarper& warper)
{
    for (auto& labelStruct: mLabels) {
        labelStruct.selectedRegion.setTimes(
            warper.Warp(labelStruct.getT0()),
            warper.Warp(labelStruct.getT1()));
    }

    // This should not be needed, assuming the warper is nondecreasing, but
    // let's not assume too much.
    SortLabels();
}

LabelStruct::LabelStruct(const SelectedRegion& region,
                         const wxString& aTitle)
    : selectedRegion(region)
    , title(aTitle)
    , mId(++s_lastLabelId)
{
    updated = false;
    width = 0;
    x = 0;
    x1 = 0;
    xText = 0;
    y = 0;
}

LabelStruct::LabelStruct(const SelectedRegion& region,
                         double t0, double t1,
                         const wxString& aTitle)
    : selectedRegion(region)
    , title(aTitle)
    , mId(++s_lastLabelId)
{
    // Overwrite the times
    selectedRegion.setTimes(t0, t1);

    updated = false;
    width = 0;
    x = 0;
    x1 = 0;
    xText = 0;
    y = 0;
}

void LabelTrack::SetSelected(bool s)
{
    bool selected = GetSelected();
    Track::SetSelected(s);
    if (selected != GetSelected()) {
        Publish({ LabelTrackEvent::Selection,
                  this->SharedPointer<LabelTrack>(), {}, -1, -1 });
    }
}

Track::Holder LabelTrack::Clone(bool) const
{
    auto result = std::make_shared<LabelTrack>(*this, ProtectedCreationArg {});
    result->Init(*this);
    return result;
}

Track::Holder LabelTrack::TrackEmptyCopy() const
{
    // not implemented
    return {};
}

// Adjust label's left or right boundary, depending which is requested.
// Return true iff the label flipped.
bool LabelStruct::AdjustEdge(int iEdge, double fNewTime)
{
    updated = true;
    if (iEdge < 0) {
        return selectedRegion.setT0(fNewTime);
    } else {
        return selectedRegion.setT1(fNewTime);
    }
}

// We're moving the label.  Adjust both left and right edge.
void LabelStruct::MoveLabel(int iEdge, double fNewTime)
{
    double fTimeSpan = getDuration();

    if (iEdge < 0) {
        selectedRegion.setTimes(fNewTime, fNewTime + fTimeSpan);
    } else {
        selectedRegion.setTimes(fNewTime - fTimeSpan, fNewTime);
    }
    updated = true;
}

static double SubRipTimestampToDouble(const wxString& ts)
{
    wxString::const_iterator end;
    wxDateTime dt;

    if (!dt.ParseFormat(ts, wxT("%H:%M:%S,%l"), &end) || end != ts.end()) {
        throw LabelStruct::BadFormatException{};
    }

    return dt.GetHour() * 3600 + dt.GetMinute() * 60 + dt.GetSecond()
           + dt.GetMillisecond() / 1000.0;
}

LabelStruct LabelStruct::Import(wxTextFile& file, int& index, LabelFormat format)
{
    SelectedRegion sr;
    wxString title;

    wxString firstLine = file.GetLine(index++);

    switch (format) {
    case LabelFormat::TEXT:
    {
        static const wxString continuation{ wxT("\\") };

        {
            // Assume tab is an impossible character within the exported text
            // of the label, so can be only a delimiter.  But other white space may
            // be part of the label text.
            wxStringTokenizer toker { firstLine, wxT("\t") };

            //get the timepoint of the left edge of the label.
            auto token = toker.GetNextToken();

            double t0;
            if (!Internat::CompatibleToDouble(token, &t0)) {
                throw BadFormatException{};
            }

            token = toker.GetNextToken();

            double t1;
            if (!Internat::CompatibleToDouble(token, &t1)) {
                //s1 is not a number.
                t1 = t0; //This is a one-sided label; t1 == t0.
            } else {
                token = toker.GetNextToken();
            }

            sr.setTimes(t0, t1);

            title = token;
        }

        // Newer selection fields are written on additional lines beginning with
        // '\' which is an impossible numerical character that older versions of
        // audacity will ignore.  Test for the presence of such a line and then
        // parse it if we can.

        // There may also be additional continuation lines from future formats that
        // we ignore.

        // Advance index over all continuation lines first, before we might throw
        // any exceptions.
        int index2 = index;
        while (index < (int)file.GetLineCount()
               && file.GetLine(index).StartsWith(continuation)) {
            ++index;
        }

        if (index2 < index) {
            wxStringTokenizer toker { file.GetLine(index2++), wxT("\t") };
            auto token = toker.GetNextToken();
            if (token != continuation) {
                throw BadFormatException{};
            }

            token = toker.GetNextToken();
            double f0;
            if (!Internat::CompatibleToDouble(token, &f0)) {
                throw BadFormatException{};
            }

            token = toker.GetNextToken();
            double f1;
            if (!Internat::CompatibleToDouble(token, &f1)) {
                throw BadFormatException{};
            }

            sr.setFrequencies(f0, f1);
        }
        break;
    }
    case LabelFormat::SUBRIP:
    {
        if ((int)file.GetLineCount() < index + 2) {
            throw BadFormatException{};
        }

        long identifier;
        // The first line should be a numeric counter; we can ignore it otherwise
        if (!firstLine.ToLong(&identifier)) {
            throw BadFormatException{};
        }

        wxString timestamp = file.GetLine(index++);
        // Parsing data of the form 'HH:MM:SS,sss --> HH:MM:SS,sss'
        // Assume that the line is in exactly that format, with no extra whitespace
        // and less than 24 hours.
        if (timestamp.length() != 29) {
            throw BadFormatException{};
        }
        if (!timestamp.substr(12, 5).IsSameAs(" --> ")) {
            throw BadFormatException{};
        }

        double t0 = SubRipTimestampToDouble(timestamp.substr(0, 12));
        double t1 = SubRipTimestampToDouble(timestamp.substr(17, 12));

        sr.setTimes(t0, t1);

        // Assume that if there is an empty subtitle, there still is a second
        // blank line after it
        title = file[index++];

        // Labels in audacity should be only one line, so join multiple lines
        // with spaces.  This is not reversed on export.
        while (index < (int)file.GetLineCount()
               && !file.GetLine(index).IsEmpty()) {
            title += " " + file.GetLine(index);
            index++;
        }

        index++; // Skip over empty line

        break;
    }
    default:
        throw BadFormatException{};
    }

    return LabelStruct{ sr, title };
}

static wxString SubRipTimestampFromDouble(double timestamp, bool webvtt)
{
    // Note that the SubRip format always uses the comma as its separator...
    static constexpr auto subripFormat = wxT("%H:%M:%S,%l");
    // ... while WebVTT always used the period.
    // WebVTT also allows skipping the hour part, but doesn't require doing so.
    static constexpr auto webvttFormat = wxT("%H:%M:%S.%l");

    // dt is the datetime that is timestamp seconds after Jan 1, 1970 UTC.
    wxDateTime dt { (time_t)timestamp };
    dt.SetMillisecond(wxRound(timestamp * 1000) % 1000);

    // As such, we need to use UTC when formatting it, or else the time will
    // be shifted (assuming the user is not in the UTC timezone).
    return dt.Format(webvtt ? webvttFormat : subripFormat, wxDateTime::UTC);
}

void LabelStruct::Export(wxTextFile& file, LabelFormat format, int index) const
{
    switch (format) {
    case LabelFormat::TEXT:
    default:
    {
        file.AddLine(wxString::Format(wxT("%s\t%s\t%s"),
                                      Internat::ToString(getT0(), FLT_DIG),
                                      Internat::ToString(getT1(), FLT_DIG),
                                      title
                                      ));

        // Do we need more lines?
        auto f0 = selectedRegion.f0();
        auto f1 = selectedRegion.f1();
        if ((f0 == SelectedRegion::UndefinedFrequency
             && f1 == SelectedRegion::UndefinedFrequency)
            || LabelStyleSetting.ReadEnum()) {
            return;
        }

        // Write a \ character at the start of a second line,
        // so that earlier versions of Audacity ignore it.
        file.AddLine(wxString::Format(wxT("\\\t%s\t%s"),
                                      Internat::ToString(f0, FLT_DIG),
                                      Internat::ToString(f1, FLT_DIG)
                                      ));

        // Additional lines in future formats should also start with '\'.
        break;
    }
    case LabelFormat::SUBRIP:
    case LabelFormat::WEBVTT:
    {
        // Note that the identifier is optional in WebVTT, but required in SubRip.
        // We include it for both.
        file.AddLine(wxString::Format(wxT("%d"), index + 1));

        file.AddLine(wxString::Format(wxT("%s --> %s"),
                                      SubRipTimestampFromDouble(getT0(), format == LabelFormat::WEBVTT),
                                      SubRipTimestampFromDouble(getT1(), format == LabelFormat::WEBVTT)));

        file.AddLine(title);
        file.AddLine(wxT(""));

        break;
    }
    }
}

auto LabelStruct::RegionRelation(
    double reg_t0, double reg_t1, const LabelTrack* WXUNUSED(parent)) const
-> TimeRelations
{
    bool retainLabels = false;

    wxASSERT(reg_t0 <= reg_t1);
    gPrefs->Read(wxT("/GUI/RetainLabels"), &retainLabels);

    if (retainLabels) {
        // Desired behavior for edge cases: The length of the selection is smaller
        // than the length of the label if the selection is within the label or
        // matching exactly a (region) label.

        if (reg_t0 < getT0() && reg_t1 > getT1()) {
            return SURROUNDS_LABEL;
        } else if (reg_t1 < getT0()) {
            return BEFORE_LABEL;
        } else if (reg_t0 > getT1()) {
            return AFTER_LABEL;
        } else if (reg_t0 >= getT0() && reg_t0 <= getT1()
                   && reg_t1 >= getT0() && reg_t1 <= getT1()) {
            return WITHIN_LABEL;
        } else if (reg_t0 >= getT0() && reg_t0 <= getT1()) {
            return BEGINS_IN_LABEL;
        } else {
            return ENDS_IN_LABEL;
        }
    } else {
        // AWD: Desired behavior for edge cases: point labels bordered by the
        // selection are included within it. Region labels are included in the
        // selection to the extent that the selection covers them; specifically,
        // they're not included at all if the selection borders them, and they're
        // fully included if the selection covers them fully, even if it just
        // borders their endpoints. This is just one of many possible schemes.

        // The first test catches bordered point-labels and selected-through
        // region-labels; move it to third and selection edges become inclusive
        // WRT point-labels.
        if (reg_t0 <= getT0() && reg_t1 >= getT1()) {
            return SURROUNDS_LABEL;
        } else if (reg_t1 <= getT0()) {
            return BEFORE_LABEL;
        } else if (reg_t0 >= getT1()) {
            return AFTER_LABEL;
        }
        // At this point, all point labels should have returned.
        else if (reg_t0 > getT0() && reg_t0 < getT1()
                 && reg_t1 > getT0() && reg_t1 < getT1()) {
            return WITHIN_LABEL;
        }
        // Knowing that none of the other relations match simplifies remaining
        // tests
        else if (reg_t0 > getT0() && reg_t0 < getT1()) {
            return BEGINS_IN_LABEL;
        } else {
            return ENDS_IN_LABEL;
        }
    }
}

/// Export labels including label start and end-times.
void LabelTrack::Export(wxTextFile& f, LabelFormat format) const
{
    if (format == LabelFormat::WEBVTT) {
        f.AddLine(wxT("WEBVTT"));
        f.AddLine(wxT(""));
    }

    // PRL: to do: export other selection fields
    int index = 0;
    for (auto& labelStruct: mLabels) {
        labelStruct.Export(f, format, index++);
    }
}

LabelFormat LabelTrack::FormatForFileName(const wxString& fileName)
{
    LabelFormat format = LabelFormat::TEXT;
    if (fileName.Right(4).CmpNoCase(wxT(".srt")) == 0) {
        format = LabelFormat::SUBRIP;
    } else if (fileName.Right(4).CmpNoCase(wxT(".vtt")) == 0) {
        format = LabelFormat::WEBVTT;
    }
    return format;
}

/// Import labels, handling files with or without end-times.
void LabelTrack::Import(wxTextFile& in, LabelFormat format)
{
    if (format == LabelFormat::WEBVTT) {
        BasicUI::ShowMessageBox(XO("Importing WebVTT files is not currently supported."));
        return;
    }

    int lines = in.GetLineCount();

    mLabels.clear();
    mLabels.reserve(lines);

    //Currently, we expect a tag file to have two values and a label
    //on each line. If the second token is not a number, we treat
    //it as a single-value label.
    bool error = false;
    for (int index = 0; index < lines;) {
        try {
            // Let LabelStruct::Import advance index
            LabelStruct l { LabelStruct::Import(in, index, format) };
            mLabels.push_back(l);
        }
        catch (const LabelStruct::BadFormatException&) {
            error = true;
        }
    }
    if (error) {
        BasicUI::ShowMessageBox(XO("One or more saved labels could not be read."));
    }
    SortLabels();
}

bool LabelTrack::HandleXMLTag(const std::string_view& tag, const AttributesList& attrs)
{
    if (tag == "label") {
        SelectedRegion selectedRegion;
        wxString title;

        // loop through attrs, which is a null-terminated list of
        // attribute-value pairs
        for (auto pair : attrs) {
            auto attr = pair.first;
            auto value = pair.second;

            if (selectedRegion.HandleXMLAttribute(attr, value, "t", "t1")) {
            }
            // Bug 1905 no longer applies, as valueView has no limits anyway
            else if (attr == "title") {
                title = value.ToWString();
            }
        } // while

        // Handle files created by Audacity 1.1.   Labels in Audacity 1.1
        // did not have separate start- and end-times.
        // PRL: this superfluous now, given class SelectedRegion's internal
        // consistency guarantees
        //if (selectedRegion.t1() < 0)
        //   selectedRegion.collapseToT0();

        LabelStruct l { selectedRegion, title };
        mLabels.push_back(l);

        return true;
    } else if (tag == "labeltrack") {
        long nValue = -1;
        for (auto pair : attrs) {
            auto attr = pair.first;
            auto value = pair.second;

            if (this->Track::HandleCommonXMLAttribute(attr, value)) {
            } else if (attr == "numlabels" && value.TryGet(nValue)) {
                if (nValue < 0) {
                    wxLogWarning(wxT("Project shows negative number of labels: %d"), nValue);
                    return false;
                }
                mLabels.clear();
                mLabels.reserve(nValue);
            }
        }

        return true;
    }

    return false;
}

XMLTagHandler* LabelTrack::HandleXMLChild(const std::string_view& tag)
{
    if (tag == "label") {
        return this;
    } else {
        return NULL;
    }
}

void LabelTrack::WriteXML(XMLWriter& xmlFile) const
// may throw
{
    int len = mLabels.size();

    xmlFile.StartTag(wxT("labeltrack"));
    this->Track::WriteCommonXMLAttributes(xmlFile);
    xmlFile.WriteAttr(wxT("numlabels"), len);

    for (auto& labelStruct: mLabels) {
        xmlFile.StartTag(wxT("label"));
        labelStruct.getSelectedRegion()
        .WriteXMLAttributes(xmlFile, "t", "t1");
        // PRL: to do: write other selection fields
        xmlFile.WriteAttr(wxT("title"), labelStruct.title);
        xmlFile.EndTag(wxT("label"));
    }

    xmlFile.EndTag(wxT("labeltrack"));
}

Track::Holder LabelTrack::Cut(double t0, double t1, bool moveClips)
{
    auto tmp = Copy(t0, t1);
    Clear(t0, t1, moveClips);
    return tmp;
}

Track::Holder LabelTrack::SplitCut(double t0, double t1)
{
    // SplitCut() == Copy() + SplitDelete()

    Track::Holder tmp = Copy(t0, t1);

    if (!SplitDelete(t0, t1)) {
        return {};
    }

    return tmp;
}

Track::Holder LabelTrack::Copy(double t0, double t1, bool) const
{
    auto tmp = std::make_shared<LabelTrack>();
    tmp->Init(*this);
    const auto lt = static_cast<LabelTrack*>(tmp.get());

    for (auto& labelStruct: mLabels) {
        LabelStruct::TimeRelations relation
            =labelStruct.RegionRelation(t0, t1, this);
        if (relation == LabelStruct::SURROUNDS_LABEL) {
            LabelStruct l {
                labelStruct.selectedRegion,
                labelStruct.getT0() - t0,
                labelStruct.getT1() - t0,
                labelStruct.title
            };
            lt->mLabels.push_back(l);
        } else if (relation == LabelStruct::WITHIN_LABEL) {
            LabelStruct l {
                labelStruct.selectedRegion,
                0,
                t1 - t0,
                labelStruct.title
            };
            lt->mLabels.push_back(l);
        } else if (relation == LabelStruct::BEGINS_IN_LABEL) {
            LabelStruct l {
                labelStruct.selectedRegion,
                0,
                labelStruct.getT1() - t0,
                labelStruct.title
            };
            lt->mLabels.push_back(l);
        } else if (relation == LabelStruct::ENDS_IN_LABEL) {
            LabelStruct l {
                labelStruct.selectedRegion,
                labelStruct.getT0() - t0,
                t1 - t0,
                labelStruct.title
            };
            lt->mLabels.push_back(l);
        }
    }
    lt->mClipLen = (t1 - t0);

    return tmp;
}

bool LabelTrack::PasteOver(double t, const Track& src)
{
    auto result = src.TypeSwitch<bool>([&](const LabelTrack& sl) {
        int len = mLabels.size();
        int pos = 0;

        while (pos < len && mLabels[pos].getT0() < t) {
            pos++;
        }

        for (auto& labelStruct: sl.mLabels) {
            LabelStruct l {
                labelStruct.selectedRegion,
                labelStruct.getT0() + t,
                labelStruct.getT1() + t,
                labelStruct.title
            };
            mLabels.insert(mLabels.begin() + pos++, l);
        }

        return true;
    });

    if (!result) {
        // THROW_INCONSISTENCY_EXCEPTION; // ?
        (void)0;// intentionally do nothing
    }
    return result;
}

void LabelTrack::Paste(double t, const Track& src, bool moveClips)
{
    bool bOk = src.TypeSwitch<bool>([&](const LabelTrack& lt) {
        double shiftAmt = lt.mClipLen > 0.0 ? lt.mClipLen : lt.GetEndTime();

        ShiftLabelsOnInsert(shiftAmt, t);
        PasteOver(t, src);

        return true;
    });

    if (!bOk) {
        // THROW_INCONSISTENCY_EXCEPTION; // ?
        (void)0;// intentionally do nothing
    }
}

// This repeats the labels in a time interval a specified number of times.
bool LabelTrack::Repeat(double t0, double t1, int n)
{
    // Sanity-check the arguments
    if (n < 0 || t1 < t0) {
        return false;
    }

    double tLen = t1 - t0;

    // Insert space for the repetitions
    ShiftLabelsOnInsert(tLen * n, t1);

    // mLabels may resize as we iterate, so use subscripting
    for (unsigned int i = 0; i < mLabels.size(); ++i) {
        LabelStruct::TimeRelations relation
            =mLabels[i].RegionRelation(t0, t1, this);
        if (relation == LabelStruct::SURROUNDS_LABEL) {
            // Label is completely inside the selection; duplicate it in each
            // repeat interval
            unsigned int pos = i; // running label insertion position in mLabels

            for (int j = 1; j <= n; j++) {
                const LabelStruct& label = mLabels[i];
                LabelStruct l {
                    label.selectedRegion,
                    label.getT0() + j * tLen,
                    label.getT1() + j * tLen,
                    label.title
                };

                // Figure out where to insert
                while (pos < mLabels.size()
                       && mLabels[pos].getT0() < l.getT0()) {
                    pos++;
                }
                mLabels.insert(mLabels.begin() + pos, l);
            }
        } else if (relation == LabelStruct::BEGINS_IN_LABEL) {
            // Label ends inside the selection; ShiftLabelsOnInsert() hasn't touched
            // it, and we need to extend it through to the last repeat interval
            mLabels[i].selectedRegion.moveT1(n * tLen);
        }

        // Other cases have already been handled by ShiftLabelsOnInsert()
    }

    return true;
}

void LabelTrack::Silence(double t0, double t1, ProgressReporter)
{
    int len = mLabels.size();

    // mLabels may resize as we iterate, so use subscripting
    for (int i = 0; i < len; ++i) {
        LabelStruct::TimeRelations relation
            =mLabels[i].RegionRelation(t0, t1, this);
        if (relation == LabelStruct::WITHIN_LABEL) {
            // Split label around the selection
            const LabelStruct& label = mLabels[i];
            LabelStruct l {
                label.selectedRegion,
                t1,
                label.getT1(),
                label.title
            };

            mLabels[i].selectedRegion.setT1(t0);

            // This might not be the right place to insert, but we sort at the end
            ++i;
            mLabels.insert(mLabels.begin() + i, l);
        } else if (relation == LabelStruct::ENDS_IN_LABEL) {
            // Beginning of label to selection end
            mLabels[i].selectedRegion.setT0(t1);
        } else if (relation == LabelStruct::BEGINS_IN_LABEL) {
            // End of label to selection beginning
            mLabels[i].selectedRegion.setT1(t0);
        } else if (relation == LabelStruct::SURROUNDS_LABEL) {
            DeleteLabel(i);
            len--;
            i--;
        }
    }

    SortLabels();
}

void LabelTrack::InsertSilence(double t, double len)
{
    for (auto& labelStruct: mLabels) {
        double t0 = labelStruct.getT0();
        double t1 = labelStruct.getT1();
        if (t0 >= t) {
            t0 += len;
        }

        if (t1 >= t) {
            t1 += len;
        }
        labelStruct.selectedRegion.setTimes(t0, t1);
    }
}

int LabelTrack::GetNumLabels() const
{
    return mLabels.size();
}

const LabelStruct* LabelTrack::GetLabel(int index) const
{
    return &mLabels[index];
}

LabelStruct* LabelTrack::GetLabelById(int64_t id)
{
    for (size_t i = 0; i < mLabels.size(); ++i) {
        if (mLabels[i].GetId() == id) {
            return &mLabels[i];
        }
    }
    return nullptr;
}

int LabelTrack::GetLabelIndex(int64_t labelId) const
{
    for (size_t i = 0; i < mLabels.size(); ++i) {
        if (mLabels[i].GetId() == labelId) {
            return static_cast<int>(i);
        }
    }
    return -1;
}

int64_t LabelTrack::AddLabel(const SelectedRegion& selectedRegion,
                             const wxString& title)
{
    LabelStruct l { selectedRegion, title };

    int len = mLabels.size();
    int pos = 0;

    while (pos < len && mLabels[pos].getT0() < selectedRegion.t0()) {
        pos++;
    }

    mLabels.insert(mLabels.begin() + pos, l);

    Publish({ LabelTrackEvent::Addition,
              this->SharedPointer<LabelTrack>(), title, -1, pos });

    return l.GetId();
}

void LabelTrack::DeleteLabel(int index)
{
    wxASSERT((index < (int)mLabels.size()));
    auto iter = mLabels.begin() + index;
    const auto title = iter->title;
    mLabels.erase(iter);

    Publish({ LabelTrackEvent::Deletion,
              this->SharedPointer<LabelTrack>(), title, index, -1 });
}

void LabelTrack::DeleteLabelById(int64_t id)
{
    for (size_t i = 0; i < mLabels.size(); ++i) {
        if (mLabels[i].GetId() == id) {
            const auto title = mLabels[i].title;
            mLabels.erase(mLabels.begin() + i);

            Publish({ LabelTrackEvent::Deletion,
                      this->SharedPointer<LabelTrack>(), title, (int)i, -1 });

            break;
        }
    }
}

/// Sorts the labels in order of their starting times.
/// This function is called often (whilst dragging a label)
/// We expect them to be very nearly in order, so insertion
/// sort (with a linear search) is a reasonable choice.
void LabelTrack::SortLabels()
{
    const auto begin = mLabels.begin();
    const auto nn = (int)mLabels.size();
    int i = 1;
    while (true)
    {
        // Find the next disorder
        while (i < nn && mLabels[i - 1].getT0() <= mLabels[i].getT0()) {
            ++i;
        }
        if (i >= nn) {
            break;
        }

        // Where must element i sink to?  At most i - 1, maybe less
        int j = i - 2;
        while ((j >= 0) && (mLabels[j].getT0() > mLabels[i].getT0())) {
            --j;
        }
        ++j;

        // Now fix the disorder
        std::rotate(
            begin + j,
            begin + i,
            begin + i + 1
            );

        // Let listeners update their stored indices
        Publish({ LabelTrackEvent::Permutation,
                  this->SharedPointer<LabelTrack>(), mLabels[j].title, i, j });
    }
}

wxString LabelTrack::GetTextOfLabels(double t0, double t1) const
{
    bool firstLabel = true;
    wxString retVal;

    for (auto& labelStruct: mLabels) {
        if (labelStruct.getT0() >= t0
            && labelStruct.getT1() <= t1) {
            if (!firstLabel) {
                retVal += '\t';
            }
            firstLabel = false;
            retVal += labelStruct.title;
        }
    }

    return retVal;
}

int LabelTrack::FindNextLabel(const SelectedRegion& currentRegion)
{
    int i = -1;

    if (!mLabels.empty()) {
        int len = (int)mLabels.size();
        if (miLastLabel >= 0 && miLastLabel + 1 < len
            && currentRegion.t0() == mLabels[miLastLabel].getT0()
            && currentRegion.t0() == mLabels[miLastLabel + 1].getT0()) {
            i = miLastLabel + 1;
        } else {
            i = 0;
            if (currentRegion.t0() < mLabels[len - 1].getT0()) {
                while (i < len
                       && mLabels[i].getT0() <= currentRegion.t0()) {
                    i++;
                }
            }
        }
    }

    miLastLabel = i;
    return i;
}

int LabelTrack::FindPrevLabel(const SelectedRegion& currentRegion)
{
    int i = -1;

    if (!mLabels.empty()) {
        int len = (int)mLabels.size();
        if (miLastLabel > 0 && miLastLabel < len
            && currentRegion.t0() == mLabels[miLastLabel].getT0()
            && currentRegion.t0() == mLabels[miLastLabel - 1].getT0()) {
            i = miLastLabel - 1;
        } else {
            i = len - 1;
            if (currentRegion.t0() > mLabels[0].getT0()) {
                while (i >= 0
                       && mLabels[i].getT0() >= currentRegion.t0()) {
                    i--;
                }
            }
        }
    }

    miLastLabel = i;
    return i;
}
